package com.korustar.autumnal.codemodel;

import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Properties;
import java.util.Set;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.MutablePropertyValues;
import org.springframework.beans.PropertyValue;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.BeanDefinitionHolder;
import org.springframework.beans.factory.config.ConstructorArgumentValues;
import org.springframework.beans.factory.config.ConstructorArgumentValues.ValueHolder;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.GenericBeanDefinition;

import com.korustar.autumnal.AutumnalContainerBuilder;
import com.korustar.autumnal.reflect.FieldReflection;
import com.korustar.autumnal.spring.BeanDefinitionUtil;
import com.sun.codemodel.JBlock;
import com.sun.codemodel.JClassAlreadyExistsException;
import com.sun.codemodel.JCodeModel;
import com.sun.codemodel.JDocComment;
import com.sun.codemodel.JExpr;
import com.sun.codemodel.JExpression;
import com.sun.codemodel.JFieldRef;
import com.sun.codemodel.JFieldVar;
import com.sun.codemodel.JInvocation;
import com.sun.codemodel.JMethod;
import com.sun.codemodel.JMod;
import com.sun.codemodel.JType;

/**
 * Core builder for the generated 'container' class
 * 
 * @author amir
 */
public class CodeModelContainerBuilder implements AutumnalContainerBuilder {
	
	private static final String PROPERTIES_MEMBER_NAME = "properties";
	public static final String DEFAULT_CONTAINER_CLASS_NAME = "MyAutumnalContainer";
	public static final String DEFAULT_BEANHOLDER_CLASS_NAME = "BeanHolder";

	private static final String METHODNAME_CONSTRUCT_SINGLETONS = "constructSingletons";
	private static final String METHODNAME_AFTER_PROPERTIES_SET = "afterPropertiesSet";
	private static final String METHODNAME_SET_BEAN_PROPERTIES = "setBeanProperties";

	private static final String MAIN_ARGS_VARNAME = "args";
	
	private static Logger LOGGER = LoggerFactory.getLogger(CodeModelContainerBuilder.class);
	
	private CodeModelParameterBuilder parameterBuilder;
	private CodeModelBeanConstructionBuilder constructionBuilder;
	
	private boolean defineMainMethod=true;
	
	@Override
	public CodeModelContainerModel build(BeanDefinitionRegistry registry) throws JClassAlreadyExistsException {
		CodeModelContainerModel model= new CodeModelContainerModel();
		this.build(registry,model);
		return model;
	}
	
	
	public void build(BeanDefinitionRegistry registry, CodeModelContainerModel model) throws JClassAlreadyExistsException {
		defineContainerClass(model);
		defineMembers(model);
		defineAllBeans(registry, model);
		
		defineStartContainerMethod(model);
		//indented to show that these go inside the startContainer method
		{
			
			defineConstructTopLevelSingletonsMethod(registry, model);
			defineSetPropertiesMethod(registry, model);
			defineAfterPropertiesSetMethod(registry, model);
			//TODO: defineShutdownHook(registry);
		}
		
		if(this.isDefineMainMethod()) {
			defineMainMethod(model);
		}
	}
	
	private void defineAfterPropertiesSetMethod(BeanDefinitionRegistry registry, CodeModelContainerModel model) {
		JMethod callInitMethods = null;
		for(String beanDefinitionName : registry.getBeanDefinitionNames())
		{
			BeanDefinition beanDefinition = registry.getBeanDefinition(beanDefinitionName);
			if(beanDefinition instanceof GenericBeanDefinition) {
				GenericBeanDefinition genericBeanDefinition = (GenericBeanDefinition)beanDefinition;
				String initMethodName = genericBeanDefinition.getInitMethodName();
				
				if(initMethodName!=null) {
					JFieldRef beanRef =  model.getBeanAsJFieldRef(beanDefinitionName);
					JInvocation initInvocation = beanRef.invoke(initMethodName);
					if(callInitMethods==null) { // only initiate if necessary ..
						callInitMethods= model.getAppContainerClass().method(JMod.PRIVATE, model.getCodeModel().VOID, METHODNAME_AFTER_PROPERTIES_SET);
						callInitMethods._throws(Exception.class);
					}
					callInitMethods.body().add(initInvocation);
				}
			}
		}
		if(callInitMethods!=null) {
			model.getStartMethod().body().add(JExpr.invoke(callInitMethods));
		}
	}

	private void defineAllBeans(BeanDefinitionRegistry registry, CodeModelContainerModel model) {
		for(String beanDefinitionName : registry.getBeanDefinitionNames())
		{
			defineBean(beanDefinitionName, registry.getBeanDefinition(beanDefinitionName), model);
		}
	}
	
	private void defineConstructTopLevelSingletonsMethod(BeanDefinitionRegistry registry, CodeModelContainerModel model) {
		JMethod constructAll = model.getAppContainerClass().method(JMod.PRIVATE, model.getCodeModel().VOID, METHODNAME_CONSTRUCT_SINGLETONS);
		constructAll._throws(Exception.class);
		model.setConstructAll( constructAll );
		
		//placeholder configurers ...
		for(String beanDefinitionName : registry.getBeanDefinitionNames())
		{
			if (BeanDefinitionUtil.SPRINGFRAMEWORK_PROPERTY_PLACEHOLDER_CONFIGURER.equals(registry.getBeanDefinition(beanDefinitionName).getBeanClassName())) 
			{
				//construct the bean in-place
				this.constructBean(PROPERTIES_MEMBER_NAME,registry.getBeanDefinition(beanDefinitionName), model, model.getStartMethod().body(),model.getStartMethod().body());
				this.definePlaceholderProperties(beanDefinitionName,registry.getBeanDefinition(beanDefinitionName), model, model.getStartMethod().body());
			}
		}
		
		//non-placeholder configurers ...
		for(String beanDefinitionName : registry.getBeanDefinitionNames())
		{
			if (!BeanDefinitionUtil.SPRINGFRAMEWORK_PROPERTY_PLACEHOLDER_CONFIGURER.equals(registry.getBeanDefinition(beanDefinitionName).getBeanClassName())) {
				this.constructBean(beanDefinitionName,registry.getBeanDefinition(beanDefinitionName), model,model.getStartMethod().body(), model.getConstructAll().body());
			}
		}	
		JDocComment jDocComment = model.getConstructAll().javadoc();
        jDocComment.add("Constructs all Singleton Beans and adds them to the bean holder");
		model.getStartMethod().body().add(JExpr.invoke(model.getConstructAll()));
	}

	private void definePlaceholderProperties(String beanDefinitionName, BeanDefinition beanDefinition, CodeModelContainerModel model, JBlock currentBlock) {
		if (BeanDefinitionUtil.SPRINGFRAMEWORK_PROPERTY_PLACEHOLDER_CONFIGURER.equals(beanDefinition.getBeanClassName())) {
			currentBlock.directStatement("// "+beanDefinitionName);
			JFieldRef injectedBean = model.getBeanAsJFieldRef(PROPERTIES_MEMBER_NAME);
			MutablePropertyValues propertyValues = beanDefinition.getPropertyValues();
			List<PropertyValue> propertyValueList = propertyValues.getPropertyValueList();
			for(PropertyValue propertyValue : propertyValueList) {
				String propertyName = propertyValue.getName();
				Object value = propertyValue.getValue();
				if(propertyName.equals("location")) {
					LOGGER.info("loading properties from {}",value);
					this.getParameterBuilder().getPropertyHelper().loadProperties((String) value);
					//TODO: classpath props ...
					String direct= "new java.io.FileInputStream(new java.io.File( \""+value+"\"))";
					JInvocation invocation = injectedBean.invoke("load").arg(JExpr.direct(direct));
					currentBlock.add(invocation);
				} else if(propertyName.equals("locations")) {
					LOGGER.warn("! locations is not implemented yet");
				}
			}
		}
	}
	
	private void defineSetPropertiesMethod(BeanDefinitionRegistry registry, CodeModelContainerModel model) {
		JMethod wireBeans = model.getAppContainerClass().method(JMod.PRIVATE, model.getCodeModel().VOID, METHODNAME_SET_BEAN_PROPERTIES);
		JBlock currentBlock= wireBeans.body();
		for(String beanDefinitionName : registry.getBeanDefinitionNames())
		{
			defineBeanProperties(registry, model, wireBeans, currentBlock,
					beanDefinitionName, registry.getBeanDefinition(beanDefinitionName));
		}
		JDocComment jDocComment = wireBeans.javadoc();
        jDocComment.add("Calls setters, and wires beans together as necessary");
		model.getStartMethod().body().add(JExpr.invoke(wireBeans));
	}


	private JFieldVar defineBean(String beanDefinitionName,
			BeanDefinition beanDefinition, CodeModelContainerModel model) {
		String legalisedMemberName = CodeModelUtil.legaliseMemberName(beanDefinitionName);
		JType beanClass;
		if(BeanDefinitionUtil.SPRINGFRAMEWORK_FIELD_RETRIEVING_FACTORY_BEAN.equals(beanDefinition.getBeanClassName())) {
			Class<?> c= BeanDefinitionUtil.guessType(beanDefinition);
			if(c.isPrimitive()) {
				beanClass= model.getCodeModel().ref(JCodeModel.primitiveToBox.get(c));
			} else {
				beanClass= model.getCodeModel().ref(c);
			}
		} else if(BeanDefinitionUtil.SPRINGFRAMEWORK_PROPERTY_PLACEHOLDER_CONFIGURER.equals(beanDefinition.getBeanClassName())) {
			//TODO: make this a unique reference ??
			beanClass= model.getCodeModel().ref(Properties.class);
			legalisedMemberName= PROPERTIES_MEMBER_NAME;
		} else {
			beanClass= model.getCodeModel().ref(BeanDefinitionUtil.getTypeAsText(beanDefinition));
		}
		JFieldVar beanField = model.getBeanHolderClass().field(JMod.PRIVATE, beanClass, legalisedMemberName);
		return beanField;
	}
	/**
	 * TODO: Recursive ...
	 */
	private void defineBeanProperties(BeanDefinitionRegistry registry,
			CodeModelContainerModel model, JMethod wireBeans,
			JBlock currentBlock, String beanDefinitionName, 
			BeanDefinition beanDefinition) {
			wireBeans.body().directStatement("// "+beanDefinitionName);
			
			JFieldRef injectedBean = model.getBeanAsJFieldRef(beanDefinitionName);
			MutablePropertyValues propertyValues = beanDefinition.getPropertyValues();
			List<PropertyValue> propertyValueList = propertyValues.getPropertyValueList();

			//construct any post-constructor dependencies
			this.processSetterDependencies(beanDefinition, model, currentBlock);

			if (BeanDefinitionUtil.SPRINGFRAMEWORK_PROPERTY_PLACEHOLDER_CONFIGURER.equals(beanDefinition.getBeanClassName())) {
				//skip. already dealt-with
			} else if(BeanDefinitionUtil.SPRINGFRAMEWORK_FIELD_RETRIEVING_FACTORY_BEAN.equals(beanDefinition.getBeanClassName())) {
				//skip. constructor did this.
			} else {
				for(PropertyValue propertyValue : propertyValueList) {
					JExpression injectedValue;
					String propertyName = propertyValue.getName();
					Object value = propertyValue.getValue();
					
					Class<?> expectedType = FieldReflection.getFieldType(BeanDefinitionUtil.getTypeAsText(beanDefinition), propertyName);
					
					LOGGER.info("Wiring property '{}', valueType '{}', expectedType '{}'", new Object[]{propertyName, value.getClass(), expectedType});
					if(value.getClass().isAssignableFrom(BeanDefinitionHolder.class)) {
						BeanDefinitionHolder bdh = (BeanDefinitionHolder) value;
						boolean definitionRequired = this.checkDefinitionRequired(bdh, model);
						BeanDefinition nestedBeanDefinition = bdh.getBeanDefinition();
						String nestedBeanName = bdh.getBeanName();
						if(definitionRequired) {
							//construct it!
							LOGGER.warn("Trying to set an 'unconstructed' bean - construct it now ...");
							this.defineAndConstructBean(bdh, model, currentBlock);
						}
						//recurse!
						//TODO: check properties haven't been set already ...
						defineBeanProperties(registry, model, wireBeans, currentBlock, nestedBeanName, nestedBeanDefinition);
					}
					injectedValue= this.getParameterBuilder().buildParameter(value, new ParameterContext(model, currentBlock, expectedType, null));
					String methodName = FieldReflection.getSetter(propertyName);

					JInvocation invocation = injectedBean.invoke(methodName).arg(injectedValue);
					currentBlock.add(invocation);
				}
			}
	}

	

	/**
	 * check if this is a 'nested bean'
	 * 
	 * @param value
	 * @param model
	 * @param currentBlock
	 * @return
	 */
	private boolean checkDefinitionRequired(BeanDefinitionHolder value,
			CodeModelContainerModel model) {
		if(!model.getConstructedBeanNames().contains(value.getBeanName())) {
			return true;
		}
		return false;
	}
	private void defineAndConstructBean(BeanDefinitionHolder bdh, CodeModelContainerModel model, JBlock currentBlock) {
		this.defineBean(bdh.getBeanName(), bdh.getBeanDefinition(), model);
		this.constructBean(bdh.getBeanName(),bdh.getBeanDefinition(),model, currentBlock, model.getConstructAll().body());
	}

	private void constructBean(String beanDefinitionName, BeanDefinition beanDefinition, CodeModelContainerModel model, JBlock currentBlock, JBlock assignmentBlock) {
		//construct any pre-constructor dependencies
		this.processConstructorDependencies(beanDefinition, model, currentBlock);
		JExpression constructor= this.getConstructionBuilder().invokeConstructor(beanDefinition, model, currentBlock);
		JFieldRef beanAsJFieldRef = model.getBeanAsJFieldRef(beanDefinitionName);
		assignmentBlock.assign( beanAsJFieldRef, constructor );
		model.getConstructedBeanNames().add(beanDefinitionName);
	}
	
	
	private void processSetterDependencies(BeanDefinition beanDefinition,
			CodeModelContainerModel model, JBlock currentBlock) {
		MutablePropertyValues propertyValues = beanDefinition.getPropertyValues();
		List<PropertyValue> propertyValueList = propertyValues.getPropertyValueList();
		for(PropertyValue propertyValue : propertyValueList) {
			Object value = propertyValue.getValue();
			this.checkValue(value, model, currentBlock);
		}

	}
	
	@SuppressWarnings("rawtypes")
	private void checkList(Object value, CodeModelContainerModel model,
			JBlock currentBlock) {
		if(value instanceof List) {
			LOGGER.warn("Found a list {}", value);
			for(Object listItem : (List) value) {
				checkBeanDefinitionHolder(listItem, model, currentBlock);
			}
		}
	}
	
	private void checkMap(Object value, CodeModelContainerModel model,
			JBlock currentBlock) {
		if(value instanceof Map) {
			@SuppressWarnings("rawtypes")
			Map map= (Map) value;
			LOGGER.warn("Found a map {}", value);
			for(Object key : map.keySet()) {
				checkBeanDefinitionHolder(map.get(key), model, currentBlock);
			}
		}
	}


	private void checkBeanDefinitionHolder(Object value, CodeModelContainerModel model, JBlock currentBlock) {
		
		if(value instanceof BeanDefinitionHolder) {
			//check!
			BeanDefinitionHolder bdh= (BeanDefinitionHolder) value;
			LOGGER.warn("Found & constructing a BeanDefinitionHolder {}", bdh.getBeanName());
			if(!model.getConstructedBeanNames().contains(bdh.getBeanName())) {
				this.defineAndConstructBean(bdh, model, currentBlock);
			}
		}
	}

	private void processConstructorDependencies(BeanDefinition beanDefinition,
			CodeModelContainerModel model, JBlock currentBlock) {
		ConstructorArgumentValues constructorArgumentValues = beanDefinition.getConstructorArgumentValues();
		List<ValueHolder> genericArgumentValues = constructorArgumentValues.getGenericArgumentValues();
		for (ValueHolder valueHolder : genericArgumentValues) {
			this.checkValue(valueHolder.getValue(), model, currentBlock);
		}
		Set<Entry<Integer, ValueHolder>> entrySet = constructorArgumentValues.getIndexedArgumentValues().entrySet();
		for (Entry<Integer, ValueHolder> entry : entrySet) {
			this.checkValue(entry.getValue().getValue(), model, currentBlock);
		}
	}


	private void checkValue(Object value, CodeModelContainerModel model,
			JBlock currentBlock) {
		LOGGER.warn("Checking a value - {}", value.getClass());
		this.checkBeanDefinitionHolder(value, model, currentBlock);
		this.checkList(value, model, currentBlock);
		this.checkMap(value, model, currentBlock);

	}



	


	private void defineMembers(CodeModelContainerModel model) throws JClassAlreadyExistsException {
        model.setBeanHolderClass( model.getAppContainerClass()._class(JMod.PRIVATE, DEFAULT_BEANHOLDER_CLASS_NAME) );
        model.setBeanHolderField( model.getAppContainerClass().field(JMod.PRIVATE | JMod.FINAL, model.getBeanHolderClass(), "beanHolder", JExpr._new(model.getBeanHolderClass())));
        JDocComment jDocComment = model.getBeanHolderField().javadoc();
        jDocComment.add("This object holds all your singleton beans, just like a Spring BeanFactory");
        LOGGER.info("beanholder's outer: {}", model.getBeanHolderClass().outer());
	}

	private void defineStartContainerMethod(CodeModelContainerModel model) {
		model.setStartMethod(model.getAppContainerClass().method(JMod.PUBLIC, model.getCodeModel().VOID, "startContainer"));
		model.getStartMethod()._throws(Exception.class);
		JDocComment javadoc = model.getStartMethod().javadoc();
		javadoc.add("Start context. This is the entrypoint for starting the Application Context");
	}

	public void defineContainerClass(CodeModelContainerModel model) throws JClassAlreadyExistsException {
		model.setAppContainerClass(model.getCodeModel()._class(model.getContainerClassName()));
        /* Adding class level coment */
        JDocComment jDocComment = model.getAppContainerClass().javadoc();
        jDocComment.add("Despringified Application Context\n");
        jDocComment.add("\n");
        jDocComment.add("This code was generated from a Spring application context");
	}
	
	public void defineMainMethod(CodeModelContainerModel model) {
		JMethod m = model.getAppContainerClass().method(JMod.PUBLIC | JMod.STATIC, model.getCodeModel().VOID, "main");
		m.param(String[].class, MAIN_ARGS_VARNAME);
		m._throws(Exception.class);
		m.body().add(JExpr._new(model.getAppContainerClass()).invoke(model.getStartMethod()));
        JDocComment jDocComment = m.javadoc();
        jDocComment.add("Main method");
	}

	public void setDefineMainMethod(boolean defineMainMethod) {
		this.defineMainMethod = defineMainMethod;
	}

	public boolean isDefineMainMethod() {
		return this.defineMainMethod;
	}


	public void setParameterBuilder(CodeModelParameterBuilder parameterBuilder) {
		this.parameterBuilder = parameterBuilder;
	}


	public CodeModelParameterBuilder getParameterBuilder() {
		return this.parameterBuilder;
	}


	public void setConstructionBuilder(CodeModelBeanConstructionBuilder constructionBuilder) {
		this.constructionBuilder = constructionBuilder;
	}


	public CodeModelBeanConstructionBuilder getConstructionBuilder() {
		return this.constructionBuilder;
	}


}
